package org.hackystat.devcathlon.configuration;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.hackystat.devcathlon.configuration.jaxb.BaseScoreWeight;
import org.hackystat.devcathlon.configuration.jaxb.DevcathlonConfiguration;
import org.hackystat.devcathlon.configuration.jaxb.EventConfiguration;
import org.hackystat.devcathlon.configuration.jaxb.EventConfigurations;
import org.hackystat.devcathlon.configuration.jaxb.Location;
import org.hackystat.devcathlon.configuration.jaxb.Properties;
import org.hackystat.devcathlon.configuration.jaxb.Property;
import org.hackystat.devcathlon.configuration.jaxb.Service;
import org.hackystat.devcathlon.configuration.jaxb.User;
import org.hackystat.devcathlon.configuration.jaxb.WakeupInterval;

/**
 * A thread-safe singleton class that manages access to the DevcathlonConfiguration associated
 * with this system.
 * <p>
 * The Devcathlon configuration is read in from ~/.hackystat/devcathlon/configuration.xml in a
 * normally running system.  For testing purposes, there is an alternative getInstance() method
 * that takes an InputStream so you can read provide an alternative configuration.xml file. 
 * @author Philip Johnson
 */
public class ConfigurationManager implements Serializable {
  /** Supports serialization. */
  private static final long serialVersionUID = 1L;
  /** The singleton instance. */
  private static ConfigurationManager theInstance = new ConfigurationManager();
  /** The JAXB instance of the configuration.xml file. */
  private DevcathlonConfiguration config = null;
  /** The private constructor. */
  private ConfigurationManager() {
    // Do nothing for now. 
  }
  
  
  /**
   * Returns the existing DevcathlonConfiguration if it has been previously read in, or 
   * initializes and returns a new one if not. 
   * If the configuration has been previously determined, then it is returned. Otherwise, an
   * attempt is made to read in the configuration from ~/.hackystat/devcathlon/configuration.xml.
   * If that file cannot be found, or if it does not conform to the schema constraints, then
   * a ConfigurationException is thrown. 
   * @return The DevcathlonConfiguration for this system.
   * @throws ConfigurationException If the configuration.xml file cannot be found or if the file
   * does not conform to the schema constraints.  
   */
  public synchronized DevcathlonConfiguration getConfiguration () throws ConfigurationException {
    if (this.config != null) {
      return this.config;
    }
    System.out.println("Reading configuration.xml from file.");
    // Otherwise we need to initialize it.
    String filePath = System.getProperty("user.home") + "/.hackystat/devcathlon/configuration.xml";
    File definitionFile = new File(filePath);
    // Return if we can't find the passed file.
    if (!definitionFile.exists()) {
      throw new ConfigurationException("configuration.xml file missing: " + filePath);
    }
    
    // Now create an InputStream for use in creating the configuration. 
    FileInputStream inputStream = null;
    DevcathlonConfiguration config = null;
    try {
      inputStream = new FileInputStream(definitionFile);
      config = makeConfiguration(inputStream);
}
    catch (Exception e) {
      throw new ConfigurationException("Problems creating input stream", e);
    }
    finally {
      try {
        inputStream.close();
      }
      catch (Exception e) { //NOPMD
        // Can't do anything about a failure to close the stream.  
      }
    }
    return config;
  }
  
  /**
   * Returns the existing DevcathlonConfiguration if it already exists, or creates a new one
   * from inputStream if one doesn't already exist. 
   * This method is primarily useful for testing. 
   * @param inputStream The input stream containing a configuration.xml file. 
   * @return The DevcathlonConfiguration
   * @throws ConfigurationException If problems occur. 
   */
  public synchronized DevcathlonConfiguration getConfiguration(InputStream inputStream) 
  throws ConfigurationException {
    if (this.config != null) {
      return this.config;
    }
    return makeConfiguration(inputStream);
  }
  
  /**
   * Creates the DevcathlonConfiguration from the passed input stream. 
   * @param inputStream The input stream containing a configuration.xml file. 
   * @return The DevcathlonConfiguration instance created from the configuration.xml file.
   * @throws ConfigurationException If problems occur. 
   */
  private DevcathlonConfiguration makeConfiguration(InputStream inputStream) 
  throws ConfigurationException {
    
    // Now initialize our instance variable with the contents of the XML file. 
    // Validate it against the XML Schema
    try {
      JAXBContext jaxbContext = JAXBContext
      .newInstance(org.hackystat.devcathlon.configuration.jaxb.ObjectFactory.class);
      Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
      InputStream configSchema = getClass().getResourceAsStream("configuration.definition.xsd");
      SchemaFactory schemaFactory = 
        SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
      Schema schema = schemaFactory.newSchema(new StreamSource(configSchema));
      unmarshaller.setSchema(schema);
      this.config =  (org.hackystat.devcathlon.configuration.jaxb.DevcathlonConfiguration) 
      unmarshaller.unmarshal(inputStream);
    } 
    catch (Exception e) {
      throw new ConfigurationException("Failed to create DevcathlonConfiguration ", e);
    }   
    
    // If we got here, then this.config has a reasonable value. 
    return this.config;
  }
  

  /**
   * Provided in order to support testing.  Use this method to replace the current configuration
   * with one of your own choosing for testing purposes, which will be returned by all subsequent
   * calls to getConfiguration.
   * @param config The new DevcathlonConfiguration instance to use (for testing purposes.) 
   */
  public synchronized void setConfiguration(DevcathlonConfiguration config) {
    this.config = config;
  }

  /**
   * Returns the singleton instance of the ConfigurationManager.
   * @return The ConfigurationManager.
   */
  public static ConfigurationManager getInstance() {
    // make sure we have read in the configuration.
    return ConfigurationManager.theInstance;
  }
  
  /**
   * Returns all event class locations.
   * @return The locations, if provided. 
   */
  public synchronized List<String> getAllEventLocations() {
    List<String> locations = new ArrayList<String>();
    for (EventConfiguration eventConfig : config.getEventConfigurations().getEventConfiguration()) {
      locations.add(eventConfig.getLocation().getLocation());
    }
    return locations;
  }
  
  /**
   * Retruns all events specified in configurations.
   * @return The locations, if provided.
   */
  public synchronized List<String> getAllEventNames() {
    List<String> eventNames = new ArrayList<String>();
    for (EventConfiguration eventConfig : config.getEventConfigurations().getEventConfiguration()) {
      eventNames.add(eventConfig.getEvent());
    }
    return eventNames;
  }
  
  /**
   * Returns the wakeup interval for the specified event.
   * @param eventName The event of interest. 
   * @return The wakeup interval, if provided. 
   * @throws ConfigurationException If the specified event is not found in the configuration
   * file, or if the wakeup value cannot be converted to an integer. 
   */
  public synchronized int getWakeupInterval(String eventName) throws ConfigurationException {
    EventConfiguration eventConfig = this.getConfiguration(eventName);
    String wakeupString = eventConfig.getWakeupInterval().getMinutes();
    try {
      int interval = Integer.parseInt(wakeupString);
      return interval;
    }
    catch (Exception e) {
      throw new ConfigurationException("Failed to parse wakeup for event: " + eventName, e);
    }
  }
  
  /**
   * Returns the base score weight for the specified event.
   * @param eventName The event of interest. 
   * @return The base score weight, if provided. 
   * @throws ConfigurationException If the specified event is not found in the configuration
   * file, or if the base score weight cannot be converted to an integer. 
   */
  public synchronized int getBaseScoreWeight(String eventName) throws ConfigurationException {
    EventConfiguration eventConfig = this.getConfiguration(eventName);
    String weightString = eventConfig.getBaseScoreWeight().getWeight();
    try {
      int weight = Integer.parseInt(weightString);
      return weight;
    }
    catch (Exception e) {
      throw new ConfigurationException("Failed to parse base weight for event: " + eventName, e);
    }
  }
  
  /**
   * Returns the class path to the Event class. Will use for reflection and add to EventManager.
   * @param eventName The name of the event.
   * @return The class path to the Event.
   * @throws ConfigurationException If the specified event is not found in the file.
   */
  public synchronized String getLocation (String eventName) throws ConfigurationException {
    EventConfiguration eventConfig = this.getConfiguration(eventName);
    String locationString = eventConfig.getLocation().getLocation();
    return locationString;
  }

  /**
   * Returns the value associated with the property with key for event, or throws an exception
   * if not found.
   * @param eventName The event of interest.
   * @param key The key whose value is to be returned. 
   * @return The value associated with key for the event. 
   * @throws ConfigurationException If the event or the key cannot be found.
   */
  public synchronized String getProperty(String eventName, String key) 
  throws ConfigurationException {
    EventConfiguration eventConfig = this.getConfiguration(eventName);
    for (Property property : eventConfig.getProperties().getProperty()) {
        if (property.getKey().equals(key)) {
          return property.getValue();
        }
    }
    throw new ConfigurationException("Could not find property with key: " + key);
  }
  
  /**
   * Return the EventConfiguration associated with eventName, or throw an exception if we couldn't
   * find it. 
   * @param eventName The event name of interest. 
   * @return The associated Event Configuration. 
   * @throws ConfigurationException If we couldn't find the eventconfiguration. 
   */
  public synchronized EventConfiguration getConfiguration(String eventName) 
  throws ConfigurationException {
    for (EventConfiguration eventConfig : config.getEventConfigurations().getEventConfiguration()) {
      if (eventConfig.getEvent().equalsIgnoreCase(eventName)) {
        return eventConfig;
      }
    }
    throw new ConfigurationException("Could not find EventConfiguration for: " + eventName);
  }
  
  /**
   * Sets the BaseScoreWeight for the passed EventConfiguration. 
   * If the EventConfiguration does not exist, it is created. 
   * @param eventName The event name.
   * @param weight The Base Score weight. 
   */
  public synchronized void setBaseScoreWeight(String eventName, int weight) {
    EventConfiguration eventConfig;
    try {
      eventConfig = getConfiguration(eventName);
    }
    catch (Exception e) {
      eventConfig = new EventConfiguration();
      eventConfig.setEvent(eventName);
      addEventConfiguration(eventConfig);
    }
    BaseScoreWeight theWeight = new BaseScoreWeight();
    theWeight.setWeight(String.valueOf(weight));
    eventConfig.setBaseScoreWeight(theWeight);
  }
  
  /**
   * Sets the WakeupInterval for the passed EventConfiguration. 
   * If the EventConfiguration does not exist, it is created. 
   * @param eventName The event name.
   * @param wakeup The wakeup interval in minutes. 
   */
  public synchronized void setWakeupInterval(String eventName, int wakeup) {
    EventConfiguration eventConfig;
    try {
      eventConfig = getConfiguration(eventName);
    }
    catch (Exception e) {
      eventConfig = new EventConfiguration();
      eventConfig.setEvent(eventName);
      addEventConfiguration(eventConfig);
    }
    WakeupInterval interval = new WakeupInterval();
    interval.setMinutes(String.valueOf(wakeup));
    eventConfig.setWakeupInterval(interval);
  }
  
  /**
   * Adds the property for the passed Event name. 
   * If the EventConfiguration does not exist, it is created. 
   * @param eventName The event name.
   * @param key The key.
   * @param value The value.
   */
  public synchronized void addProperty(String eventName, String key, String value) {
    EventConfiguration eventConfig;
    try {
      eventConfig = getConfiguration(eventName);
    }
    catch (Exception e) {
      eventConfig = new EventConfiguration();
      eventConfig.setEvent(eventName);
      addEventConfiguration(eventConfig);
    }
    Property property = new Property();
    property.setKey(key);
    property.setValue(value);
    if (eventConfig.getProperties() == null) {
      eventConfig.setProperties(new Properties());
    }
    eventConfig.getProperties().getProperty().add(property);
  }
  
  /**
   * Adds a new EventConfiguration to the DevcathlonConfiguration. 
   * Initializes the EventConfigurations instance if necessary.
   * @param eventConfig The EventConfiguration to add.
   */
  private void addEventConfiguration(EventConfiguration eventConfig) {
    if (this.config.getEventConfigurations() == null) {
      this.config.setEventConfigurations(new EventConfigurations());
    }
    this.config.getEventConfigurations().getEventConfiguration().add(eventConfig);
  }  
  
  /**
   * Returns the value associated with the property with key for user, or throws an exception
   * if not found.
   * @param user The user of interest.
   * @param key The key whose value is to be returned. 
   * @return The value associated with key for the event. 
   * @throws ConfigurationException If the user or the key cannot be found.
   */
  public synchronized String getUserProperty(User user, String key) 
  throws ConfigurationException {
    for (Property property : user.getProperties().getProperty()) {
        if (property.getKey().equals(key)) {
          return property.getValue();
        }
    }
    throw new ConfigurationException("Could not find property with key: " + key);
  }  
  

  /**
   * Returns the users provided in configuration.xml.
   * @return The list of users.  
   */
  public synchronized List<User> getUsers() {
    if (this.config == null) {
      return null;
    }
    else {
      return this.config.getUsers().getUser();    
    }
  }

  /**
   * Returns the port to be used for this Devcathlon application instance.
   * Defaults to 9870 if the Service element is not present or the port
   * attribute cannot be parsed into an integer. 
   * @return The port, such as 9870.
   * @throws ConfigurationException If the configuration.xml file cannot be parsed. 
   */
  public synchronized int getPort() throws ConfigurationException {
    getConfiguration();
    int defaultPort = 9870;
    Service service = this.config.getService();
    if (service == null) {
      return defaultPort;
    }
    try {
      int port = Integer.parseInt(service.getPort());
      return port;
    }
    catch (Exception e) {
      return defaultPort;
    }
  }
  
  /**
   * Returns the context path for this application. 
   * @return The context path, such as "devcathlon". 
   */
  public synchronized String getContextPath() {
    return "devcathlon";
  }
  
  /**
   * Sets the event's location.
   * @param eventName The event name.
   * @param location The event location.
   */
  public synchronized void setEventLocation(String eventName, String location) {
    EventConfiguration eventConfig;
    try {
      eventConfig = getConfiguration(eventName);
    }
    catch (Exception e) {
      eventConfig = new EventConfiguration();
      eventConfig.setEvent(eventName);
      addEventConfiguration(eventConfig);
    }
    
    Location newLocation = new Location();
    newLocation.setLocation(location);
    eventConfig.setLocation(newLocation);
  }
  
}
